/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js";
import {
    CAE_CONSTANTS,
    DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX,
    DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT,
    DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT,
    MANAGED_IDENTITY_RESOURCE,
    MANAGED_IDENTITY_RESOURCE_ID,
    MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR,
    TEST_CONFIG,
} from "../../test_kit/StringConstants.js";

import {
    userAssignedClientIdConfig,
    managedIdentityRequestParams,
    systemAssignedConfig,
    ManagedIdentityNetworkErrorClient,
    networkClient,
    userAssignedResourceIdConfig,
} from "../../test_kit/ManagedIdentityTestUtils.js";
import {
    AuthenticationResult,
    HttpStatus,
    ServerError,
} from "@azure/msal-common";
import { ManagedIdentityClient } from "../../../src/client/ManagedIdentityClient.js";
import {
    ManagedIdentityEnvironmentVariableNames,
    ManagedIdentityQueryParameters,
    ManagedIdentitySourceNames,
} from "../../../src/utils/Constants.js";
import { ManagedIdentityUserAssignedIdQueryParameterNames } from "../../../src/client/ManagedIdentitySources/BaseManagedIdentitySource.js";

describe("Acquires a token successfully via an App Service Managed Identity", () => {
    beforeAll(() => {
        process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT] =
            "fake_IDENTITY_ENDPOINT";
        process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER] =
            "fake_IDENTITY_HEADER";
        process.env[
            ManagedIdentityEnvironmentVariableNames.IDENTITY_SERVER_THUMBPRINT
        ] = "fake_IDENTITY_SERVER_THUMBPRINT";
    });

    afterAll(() => {
        delete process.env[
            ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT
        ];
        delete process.env[
            ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER
        ];
        delete process.env[
            ManagedIdentityEnvironmentVariableNames.IDENTITY_SERVER_THUMBPRINT
        ];
    });

    afterEach(() => {
        // reset static variables after each test
        delete ManagedIdentityClient["identitySource"];
        delete ManagedIdentityApplication["nodeStorage"];
        jest.restoreAllMocks();
    });

    describe("User Assigned", () => {
        test("acquires a User Assigned Client Id token", async () => {
            const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn(
                networkClient,
                <any>"sendGetRequestAsync"
            );

            const managedIdentityApplication: ManagedIdentityApplication =
                new ManagedIdentityApplication(userAssignedClientIdConfig);
            expect(managedIdentityApplication.getManagedIdentitySource()).toBe(
                ManagedIdentitySourceNames.SERVICE_FABRIC
            );

            const networkManagedIdentityResult: AuthenticationResult =
                await managedIdentityApplication.acquireToken(
                    managedIdentityRequestParams
                );
            expect(networkManagedIdentityResult.accessToken).toEqual(
                DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken
            );

            const url: URLSearchParams = new URLSearchParams(
                sendGetRequestAsyncSpy.mock.lastCall[0]
            );
            expect(
                url.has(
                    ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID
                )
            ).toBe(true);
            expect(
                url.has(
                    ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017
                )
            ).toBe(false);
        });
        test("acquires a User Assigned Resource Id token", async () => {
            const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn(
                networkClient,
                <any>"sendGetRequestAsync"
            );

            const managedIdentityApplication: ManagedIdentityApplication =
                new ManagedIdentityApplication(userAssignedResourceIdConfig);
            expect(managedIdentityApplication.getManagedIdentitySource()).toBe(
                ManagedIdentitySourceNames.SERVICE_FABRIC
            );

            const networkManagedIdentityResult: AuthenticationResult =
                await managedIdentityApplication.acquireToken(
                    managedIdentityRequestParams
                );

            expect(networkManagedIdentityResult.accessToken).toEqual(
                DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken
            );

            const url: URLSearchParams = new URLSearchParams(
                sendGetRequestAsyncSpy.mock.lastCall[0]
            );
            expect(
                url.has(
                    ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS
                )
            ).toBe(true);
            expect(
                url.get(
                    ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS
                )
            ).toEqual(MANAGED_IDENTITY_RESOURCE_ID);

            jest.restoreAllMocks();
        });
    });

    describe("System Assigned", () => {
        let managedIdentityApplication: ManagedIdentityApplication;
        beforeEach(() => {
            managedIdentityApplication = new ManagedIdentityApplication(
                systemAssignedConfig
            );
            expect(managedIdentityApplication.getManagedIdentitySource()).toBe(
                ManagedIdentitySourceNames.SERVICE_FABRIC
            );
        });

        test("acquires a token", async () => {
            const networkManagedIdentityResult: AuthenticationResult =
                await managedIdentityApplication.acquireToken(
                    managedIdentityRequestParams
                );
            expect(networkManagedIdentityResult.fromCache).toBe(false);

            expect(networkManagedIdentityResult.accessToken).toEqual(
                DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken
            );
        });

        test("returns an already acquired token from the cache", async () => {
            const networkManagedIdentityResult: AuthenticationResult =
                await managedIdentityApplication.acquireToken({
                    resource: MANAGED_IDENTITY_RESOURCE,
                });
            expect(networkManagedIdentityResult.fromCache).toBe(false);

            expect(networkManagedIdentityResult.accessToken).toEqual(
                DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken
            );

            const cachedManagedIdentityResult: AuthenticationResult =
                await managedIdentityApplication.acquireToken({
                    resource: MANAGED_IDENTITY_RESOURCE,
                });
            expect(cachedManagedIdentityResult.fromCache).toBe(true);
            expect(cachedManagedIdentityResult.accessToken).toEqual(
                DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken
            );
        });
    });

    describe("Miscellaneous", () => {
        it.each([
            [
                CAE_CONSTANTS.CLIENT_CAPABILITIES,
                CAE_CONSTANTS.CLIENT_CAPABILITIES.toString(),
            ],
            [undefined, null],
        ])(
            "ignores a cached token when claims are provided (regardless of if client capabilities are provided or not) and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh is included in the network request to the Managed Identity",
            async (providedCapabilities, capabilitiesOnNetworkRequest) => {
                const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn(
                    networkClient,
                    <any>"sendGetRequestAsync"
                );

                const managedIdentityApplication: ManagedIdentityApplication =
                    new ManagedIdentityApplication({
                        ...systemAssignedConfig,
                        clientCapabilities: providedCapabilities,
                    });
                expect(
                    managedIdentityApplication.getManagedIdentitySource()
                ).toBe(ManagedIdentitySourceNames.SERVICE_FABRIC);

                let networkManagedIdentityResult: AuthenticationResult =
                    await managedIdentityApplication.acquireToken({
                        resource: MANAGED_IDENTITY_RESOURCE,
                    });
                expect(networkManagedIdentityResult.fromCache).toBe(false);
                expect(networkManagedIdentityResult.accessToken).toEqual(
                    DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken
                );

                expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(1);
                const firstNetworkRequestUrlParams: URLSearchParams =
                    new URLSearchParams(
                        sendGetRequestAsyncSpy.mock.lastCall[0]
                    );
                expect(
                    firstNetworkRequestUrlParams.get(
                        ManagedIdentityQueryParameters.XMS_CC
                    )
                ).toEqual(capabilitiesOnNetworkRequest);

                const cachedManagedIdentityResult: AuthenticationResult =
                    await managedIdentityApplication.acquireToken({
                        resource: MANAGED_IDENTITY_RESOURCE,
                    });
                expect(cachedManagedIdentityResult.fromCache).toBe(true);
                expect(cachedManagedIdentityResult.accessToken).toEqual(
                    DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken
                );
                expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(1);

                networkManagedIdentityResult =
                    await managedIdentityApplication.acquireToken({
                        claims: TEST_CONFIG.CLAIMS,
                        resource: MANAGED_IDENTITY_RESOURCE,
                    });
                expect(networkManagedIdentityResult.fromCache).toBe(false);
                expect(networkManagedIdentityResult.accessToken).toEqual(
                    DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken
                );

                expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(2);
                const secondNetworkRequestUrlParams: URLSearchParams =
                    new URLSearchParams(
                        sendGetRequestAsyncSpy.mock.lastCall[0]
                    );
                expect(
                    secondNetworkRequestUrlParams.get(
                        ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH
                    )
                ).toEqual(
                    DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX
                );
            }
        );
    });

    describe("Errors", () => {
        test("ensures that the error format is correct", async () => {
            const managedIdentityNetworkErrorClient400 =
                new ManagedIdentityNetworkErrorClient(
                    MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR,
                    undefined,
                    HttpStatus.BAD_REQUEST
                );

            jest.spyOn(networkClient, <any>"sendGetRequestAsync")
                // permanently override the networkClient's sendGetRequestAsync method to return a 400
                .mockReturnValue(
                    managedIdentityNetworkErrorClient400.sendGetRequestAsync()
                );

            const managedIdentityApplication: ManagedIdentityApplication =
                new ManagedIdentityApplication(systemAssignedConfig);
            expect(managedIdentityApplication.getManagedIdentitySource()).toBe(
                ManagedIdentitySourceNames.SERVICE_FABRIC
            );

            let serverError: ServerError = new ServerError();
            try {
                await managedIdentityApplication.acquireToken(
                    managedIdentityRequestParams
                );
            } catch (e) {
                serverError = e as ServerError;
            }

            expect(
                serverError.errorMessage.includes(
                    MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.error as string
                )
            ).toBe(true);
            expect(
                serverError.errorMessage.includes(
                    MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.error_description as string
                )
            ).toBe(true);
            MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.error_codes?.forEach(
                (errorCode) => {
                    expect(serverError.errorMessage.includes(errorCode)).toBe(
                        true
                    );
                }
            );
            expect(
                serverError.errorMessage.includes(
                    MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.timestamp as string
                )
            ).toBe(true);
            expect(
                serverError.errorMessage.includes(
                    MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.trace_id as string
                )
            ).toBe(true);
            expect(
                serverError.errorMessage.includes(
                    MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR.correlation_id as string
                )
            ).toBe(true);
        });
    });
});
